﻿// || - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ||
// || euzo.ca                                                                         | 2153735 Ontario Ltd. | Mississauga, Ontario, CANADA ||
// || - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ||

#region Implemented Namespaces

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Runtime.InteropServices;
using System.IO;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Linq;

using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell;
using OLE = Microsoft.VisualStudio.OLE.Interop;
using System.Xml.Linq;

#endregion

namespace EuzoCa.CodeGenerator
{
    /// <summary>
    /// Run the xsl template, and return the results as a C# code file.
    /// </summary>
    [Guid("33E85A40-E69D-47f7-89FA-2614C1831B4A")]
    public class CodeGenerator : IVsSingleFileGenerator, OLE::IObjectWithSite
    {
        /// <summary>
        /// A reference to the code generation progress.
        /// </summary>
        private IVsGeneratorProgress GenerateProgress;

        /// <summary>
        /// The default namespace.
        /// </summary>
        public string DefaultNamespace;

        /// <summary>
        /// The folder path where the file to convert is.
        /// </summary>
        public string InputFolderPath;

        /// <summary>
        /// The input file name, excluding the path.
        /// </summary>
        public string InputFileName;

        /// <summary>
        /// The input file extension.
        /// </summary>
        public string InputFileExtension;

        /// <summary>
        /// It is C# by default, unless specified otherwise.
        /// </summary>
        public string OutputFileExtension = ".cs";

        /// <summary>
        /// The site, provided by VS.NET.
        /// </summary>
        private object _Site;

        /// <summary>
        /// Perform the Xml/Xsl to Cs transformation.
        /// </summary>        
        /// <param name="inputFileContent">The file contents as a string.</param>
        /// <returns>The transformation results in a byte array.</returns>
        public byte[] TransformXsl(string inputFileContent)
        {
            XDocument template = null;
            XDocument document = null;

            // Find if the custom tool is set directly on the XSL template, if it is, load the xsl template.
            if (this.InputFileExtension == ".xsl" || this.InputFileExtension == ".xslt")
            {
                document = XDocument.Parse("<EmptyDocument></EmptyDocument>");
                template = XDocument.Parse(inputFileContent);
            }
            // If the custom tool is assigned to an xml file, extract the xsl template from the processing instructions of the xml file.
            else
            {
                document = XDocument.Parse(inputFileContent);
                XProcessingInstruction processingInstruction = document.Nodes().OfType<XProcessingInstruction>().FirstOrDefault();
                if (processingInstruction == null)
                {
                    throw new Exception("Please assign an xsl template to the xml file");
                }

                string xsltFileName = null;
                string processingInstructionStr = processingInstruction.ToString();
                int templateHref = processingInstructionStr.IndexOf("href", 16);
                if (templateHref >= 0)
                {
                    templateHref = processingInstructionStr.IndexOf("\"", templateHref);
                    int templateHrefEnd = processingInstructionStr.IndexOf("\"", templateHref + 1);
                    if (templateHrefEnd - templateHref <= 10000)
                    {
                        xsltFileName = processingInstructionStr.Substring(templateHref + 1,
                            templateHrefEnd - templateHref - 1);
                    }
                }

                if (xsltFileName == null)
                {
                    throw new Exception("Unable to resolve the name of the xsl template that is assigned to this xml file");
                }
                if (!xsltFileName.Contains(":"))
                {
                    xsltFileName = this.InputFolderPath + "\\" + xsltFileName;
                }
                template = XDocument.Load(xsltFileName);
            }

            // Find the default extension for the newly generated file, otherwise the default will be .cs
            XElement xslOutput = template.Element("{http://www.w3.org/1999/XSL/Transform}stylesheet").
                Element("{http://www.w3.org/1999/XSL/Transform}output");
            XAttribute defaultExtension = xslOutput.Attribute("doctype-public");
            if (defaultExtension != null)
            {
                this.OutputFileExtension = defaultExtension.Value;
            }

            // Run the transform
            MemoryStream results = new MemoryStream();
            StreamWriter writer = new StreamWriter(results);

            XsltArgumentList arg = new XsltArgumentList();
            CodeGeneratorUtil cgutil = new CodeGeneratorUtil();
            cgutil.DefaultNamespace = this.DefaultNamespace;
            cgutil.FileName = this.InputFileName;
            cgutil.FolderPath = this.InputFolderPath;
            arg.AddExtensionObject("http://www.codeplex.com/codegenerator", cgutil);

            XslCompiledTransform transformer = new XslCompiledTransform();
            XsltSettings settings = new XsltSettings(true, true);
            transformer.Load(template.CreateNavigator(), settings, null);

            XAttribute indent = xslOutput.Attribute("indent");
            XAttribute method = xslOutput.Attribute("method");
            if (indent != null && indent.Value == "yes" && method != null && method.Value == "xml")
            {
                XmlTextWriter xmlWriter = new XmlTextWriter(writer);
                xmlWriter.Formatting = Formatting.Indented;
                transformer.Transform(document.CreateNavigator(), arg, xmlWriter);
            }
            else
            {
                transformer.Transform(document.CreateNavigator(), arg, writer);
            }

            results.Seek(0, SeekOrigin.Begin);
            byte[] buffer = new byte[results.Length];
            results.Read(buffer, 0, (int)results.Length);

            return buffer;
        }

        /// <summary>
        /// Return the default extension of the generated file, Cs/Xsl inline is currently supporting 
        /// Xml -> C#, but if you are planning to support C# <-> C#, or VB, implement the file naming 
        /// logic here.
        /// </summary>
        /// <param name="pbstrDefaultExtension">The default extension (a return parameter).</param>
        /// <returns>0 in case of no errors I guess :-)</returns>
        public int DefaultExtension(out string pbstrDefaultExtension)
        {
            pbstrDefaultExtension = this.OutputFileExtension;
            return this.OutputFileExtension.Length;
        }

        /// <summary>
        /// Run the custom code generator.
        /// </summary>
        /// <param name="wszInputFilePath">The name of the input file.</param>
        /// <param name="bstrInputFileContents">The contents of the input file.</param>
        /// <param name="wszDefaultNamespace">The default namespace, when applicable.</param>
        /// <param name="rgbOutputFileContents">A reference to the output file contents.</param>
        /// <param name="pcbOutput">The output contents length.</param>
        /// <param name="pGenerateProgress">A reference to the progress bar management object.</param>
        /// <returns>0 in case of no errors I guess :-)</returns>
        public int Generate(string wszInputFilePath, string bstrInputFileContents, string wszDefaultNamespace,
            IntPtr[] rgbOutputFileContents, out uint pcbOutput, IVsGeneratorProgress pGenerateProgress)
        {
            this.DefaultNamespace = wszDefaultNamespace;
            this.GenerateProgress = pGenerateProgress;

            pcbOutput = 0;
            rgbOutputFileContents[0] = IntPtr.Zero;

            try
            {
                FileInfo inputFileInfo = new FileInfo(wszInputFilePath);
                this.InputFolderPath = inputFileInfo.DirectoryName.ToLower();
                this.InputFileName = inputFileInfo.Name.ToLower();
                this.InputFileExtension = inputFileInfo.Extension.ToLower();

                byte[] results = this.TransformXsl(bstrInputFileContents);
                if (results != null && results.Length > 0)
                {
                    rgbOutputFileContents[0] = Marshal.AllocCoTaskMem(results.Length);
                    Marshal.Copy(results, 0, rgbOutputFileContents[0], results.Length);
                    pcbOutput = (uint)results.Length;
                }
            }
            catch (Exception exception)
            {
                if (pGenerateProgress != null)
                {
                    pGenerateProgress.GeneratorError(0, 0, exception.ToString(), 0, 0);
                    return -1;
                }
            }

            return 0;
        }

        /// <summary>
        /// Retrieves the last site set with IObjectWithSite::SetSite. If there's no known site, 
        /// the object returns a failure code.
        /// </summary>
        /// <param name="riid">The IID of the interface pointer that should be returned 
        /// in ppvSite. </param>
        /// <param name="ppvSite">Address of the caller's void* variable in which the object 
        /// stores the interface pointer of the site last seen in IObjectWithSite::SetSite. 
        /// The specific interface returned depends in the riid argumentin essence, 
        /// the two arguments act identically to those in QueryInterface. If the appropriate 
        /// interface pointer is available, the object must call AddRef on that pointer before 
        /// returning successfully. If no site is available, or the requested interface is not
        /// supported, the object sets this argument to NULL and returns a failure code. 
        /// </param>
        public void GetSite(ref Guid riid, out IntPtr ppvSite)
        {
            if (this._Site == null)
            {
                throw new Win32Exception(-2147467259);
            }

            IntPtr objectPointer = Marshal.GetIUnknownForObject(this._Site);

            try
            {
                Marshal.QueryInterface(objectPointer, ref riid, out ppvSite);
                if (ppvSite == IntPtr.Zero)
                {
                    throw new Win32Exception(-2147467262);
                }
            }
            finally
            {
                if (objectPointer != IntPtr.Zero)
                {
                    Marshal.Release(objectPointer);
                    objectPointer = IntPtr.Zero;
                }
            }
        }

        /// <summary>
        /// Provides the site's IUnknown pointer to the object.
        /// </summary>
        /// <param name="pUnkSite">Address of an interface pointer to the site managing this object. 
        /// If NULL, the object should call IUnknown::Release to release the existing site.
        /// </param>
        public void SetSite(object pUnkSite)
        {
            this._Site = pUnkSite;
        }
    }
}
